import os
import glob
from pathlib import Path
from typing import List
from thefuzz import fuzz

from chernc.constants import DEFAULT_LOGGER_DIR, DEFAULT_LOGGER_FILE_NAME
from chernc.logging import build_logger

logger = build_logger(__name__ + ".manual_manager", DEFAULT_LOGGER_FILE_NAME, logger_dir=DEFAULT_LOGGER_DIR)


class ManualDocument(object):
    def __init__(self, path: str, name: str, labels: List[str]) -> None:
        self.path = path
        self.name = name
        self.labels = labels

    def __str__(self) -> str:
        return f"<{self.name}> {self.labels}"

    def __repr__(self) -> str:
        return str(self)


def same_label_count(labels: List[str], model_labels: List[str]) -> int:
    match_num = 0
    for label in labels:
        for mi, mlabel in enumerate(model_labels):
            if label.strip().lower() == mlabel.strip().lower() or label.strip().lower() in mlabel.strip().lower():
                match_num += mi + 1
    return match_num


class ManualBotManager(object):
    RUSTUP_DOC = "~/.rustup/toolchains/"

    def __init__(self) -> None:
        self.doc_path = self._get_rust_doc_path()
        self.documents = self.get_all_html(self.doc_path)

    def _get_rust_doc_path(self) -> str:
        versions = glob.glob(os.path.join(os.path.expanduser(self.RUSTUP_DOC), "*"))
        if len(versions) == 0:
            raise Exception("No rust toolchains found")
        selected_version = None
        for version in versions:
            if "nightly" in version:
                selected_version = os.path.basename(version)
                break
        if not selected_version:
            selected_version = versions[0]

        # doc path: ~/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/share/doc/rust/html/
        doc_path = os.path.join(os.path.expanduser(self.RUSTUP_DOC), selected_version, "share/doc/rust/html/")
        return doc_path

    def get_all_html(self, path: str) -> List[ManualDocument]:
        logger.info(f"Loading manual from {path}")
        # get all htmls recursively
        manual_files = []
        for root, dirs, files in os.walk(path):
            for file in files:
                if file.endswith(".html"):
                    manual_files.append(os.path.join(root, file))
        docs = []
        for file in manual_files:
            base_name = os.path.basename(file)
            doc = ManualDocument(
                file, str(Path(base_name).with_suffix("")), os.path.relpath(file, path).replace(".html", "").split("/")
            )
            docs.append(doc)
        logger.info(f"Loaded {len(docs)} manual documents")
        return docs

    def levenshtein_distance(self, s1: str, s2: str) -> int:
        """
        Calculate the Levenshtein distance between two strings s1 and s2.
        """
        if len(s1) > len(s2):
            s1, s2 = s2, s1

        distances = range(len(s1) + 1)
        for i2, c2 in enumerate(s2):
            distances_ = [i2 + 1]
            for i1, c1 in enumerate(s1):
                if c1 == c2:
                    distances_.append(distances[i1])
                else:
                    distances_.append(1 + min((distances[i1], distances[i1 + 1], distances_[-1])))
            distances = distances_
        return distances[-1]

    def search_by_name(self, name: str) -> List[ManualDocument]:
        results = [doc for doc in self.documents if name.lower() in doc.name.lower()]
        # sort by edit distance
        results.sort(key=lambda x: self.levenshtein_distance(name, x.name))
        return results

    def search_by_label(self, label: str) -> List[ManualDocument]:
        lower_label = label.lower().strip()
        results = [doc for doc in self.documents if lower_label in doc.labels]
        # sort by edit distance
        results.sort(key=lambda x: self.levenshtein_distance(label, x.name))
        return results

    def search_by_labels(self, labels: List[str]) -> List[ManualDocument]:
        cleaned_labels = [label.lower().strip() for label in labels]
        results = []
        for doc in self.documents:
            match_num = same_label_count(cleaned_labels, doc.labels)
            if match_num > 0:
                results.append(doc)

        # sort by label match num then edit distance, last label give max priority
        labels_str = " ".join(labels)
        results.sort(
            key=lambda x: (same_label_count(cleaned_labels, x.labels), -self.levenshtein_distance(labels_str, x.name)),
            reverse=True,
        )
        return results


if __name__ == "__main__":
    bot = ManualBotManager()
    print(bot.doc_path)
    # print(bot.documents)

    doc = bot.search_by_name("phi")
    print(doc[:10])

    doc = bot.search_by_labels(["core", "phi"])
    print(doc[:10])
